/*
 * Copyright 2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package net.thick4.context;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * テストのためのユーティリティー・メソッド群を含むクラスです。
 */
final class ContextTestUtil implements InvocationHandler {

	/**
	 * オブジェクトをラップするために必要な機能を定義します。
	 */
	private static interface Wrapper {
		/**
		 * ラップされているオブジェクトを取得します。
		 * @return ラップされているオブジェクト
		 */
		Object unwrap();
	}

	/** プロパティーを保持するための Map です。 */
	private Map properties = new HashMap();

	/** Proxy オブジェクトを指定するための Map です。 */
	private Map proxies;

	/** メソッドの戻り値を指定するための Map です。 */
	private Map results;

	/**
	 * 唯一のコンストラクターです。
	 * @param map メソッドの戻り値を指定するための Map
	 */
	private ContextTestUtil(Map map) {
		if ((null == map) || map.isEmpty()) {
			proxies = Collections.EMPTY_MAP;
			results = Collections.EMPTY_MAP;
			return;
		}

		proxies = new HashMap();
		results = new HashMap();
		Iterator entries = map.entrySet().iterator();
		while (entries.hasNext()) {
			Map.Entry entry = (Map.Entry) entries.next();
			Object value = entry.getValue();
			Map target;
			target = (value instanceof InvocationHandler) ? proxies : results;
			Object key = entry.getKey();
			if (key instanceof Object[]) {
				Object[] keys = (Object[]) key;
				for (int i = 0; i < keys.length; i++) {
					target.put(keys[i], value);
				}
				continue;
			}
			target.put(key, value);
		}
	}

	/**
	 * 指定された型を実装する動的プロキシーを取得します。
	 * @param clazz 動的 Proxy に実装させたい型
	 * @return 動的 Proxy のインスタンス
	 */
	static Object create(Class clazz) {
		return create(clazz, null);
	}

	/**
	 * 指定された型を実装する動的プロキシーを取得します。
	 * @param clazz 動的 Proxy に実装させたい型
	 * @param map メソッドの戻り値または InvocationHandler を含む Map
	 * @return 動的 Proxy のインスタンス
	 */
	static Object create(Class clazz, Map map) {
		Class[] types = { clazz };
		InvocationHandler proxy = new ContextTestUtil(map);
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		return Proxy.newProxyInstance(loader, types, proxy);
	}

	/**
	 * 指定されたオブジェクトを、このクラスの処理と干渉しないデータに変換します。
	 * メソッドの想定結果(戻り値)を、例外オブジェクトや
	 * {@link InvocationHandler} にしたい場合に使用する必要があります。
	 * @param object 変換対象のオブジェクト
	 * @return 変換結果
	 */
	static Object wrap(Object object) {
		final Object reference = object;
		return new Wrapper() {
			public Object unwrap() {
				return reference;
			}
		};
	}

	/**
	 * 想定結果を処理します。例外オブジェクトはスローされ、
	 * ラップされていたオブジェクトは、アンラップされます。
	 * @param object 想定結果を表すオブジェクト
	 * @return 処理済の想定結果
	 * @exception Throwable object が例外オブジェクトの場合
	 * @see #wrap(Object)
	 */
	private static Object handle(Object object) throws Throwable {
		if (object instanceof Throwable) {
			throw (Throwable) object;
		}
		if (object instanceof Wrapper) {
			return ((Wrapper) object).unwrap();
		}
		return object;
	}

	/**
	 * オブジェクトの処理をエミュレートします。
	 * @param object オブジェクト(通常は自分自身)
	 * @param method 呼び出されたメソッドをリフレクトするオブジェクト
	 * @param parameters メソッドの引数のリスト
	 * @return メソッドの実行結果
	 * @exception Throwable 処理の委譲先のメソッドが例外をスローした場合
	 */
	public Object invoke(Object object, Method method, Object[] parameters)
			throws Throwable {
		String name = method.getName();
		InvocationHandler handler = (InvocationHandler) proxies.get(name);
		if (null != handler) {
			return handler.invoke(object, method, parameters);
		}
		if (results.containsKey(name)) {
			return handle(results.get(name));
		}
		if (name.startsWith("get")) {
			return get(getPropertyName(name, "get"), parameters);
		}
		if (name.startsWith("is")) {
			return get(getPropertyName(name, "is"), parameters);
		}
		if (name.startsWith("set")) {
			return set(getPropertyName(name, "set"), parameters);
		}
		int length = (null == parameters) ? 0 : parameters.length;
		if (name.startsWith("remove") && (length == 1)) {
			name = getPropertyName(name, "remove");
			return getMap(name).remove(parameters[0]);
		}
		if (name.equals("equals") && (length == 1)) {
			return (object == parameters[0]) ? Boolean.TRUE : Boolean.FALSE;
		}
		return null;
	}

	/**
	 * getter の処理を実行します。
	 * @param property 属性名
	 * @param parameters メソッドの引数のリスト
	 * @return 取得されたプロパティーの値
	 */
	private Object get(String property, Object[] parameters) {
		switch (((null == parameters) ? 0 : parameters.length)) {
		case 0:
			return properties.get(property);

		case 1:
			return getMap(property).get(parameters[0]);

		default:
			return null;
		}
	}

	/**
	 * 指定されたプロパティーに対応する Map を取得します。
	 * @param name プロパティー名
	 * @return 指定されたプロパティーに対応する Map
	 */
	private Map getMap(String name) {
		Map map = (Map) properties.get(name);
		if (null == map) {
			map = new HashMap();
			properties.put(name, map);
		}
		return map;
	}

	/**
	 * メソッド名をプロパティー名に変換します。
	 * @param name メソッド名
	 * @param prefix プロパティー名の接頭辞となる部分
	 * @return プロパティー名
	 */
	private String getPropertyName(String name, String prefix) {
		StringBuffer buffer = new StringBuffer(name).delete(0, prefix.length());
		switch (buffer.length()) {
		case 0:
			return buffer.toString();

		case 1:
			buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0)));
			return buffer.toString();

		default:
			if (Character.isUpperCase(buffer.charAt(1))) {
				return buffer.toString();
			}
			buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0)));
			return buffer.toString();
		}
	}

	/**
	 * setter の処理を実行します。
	 * @param property 属性名
	 * @param parameters メソッドの引数のリスト
	 * @return 今まで設定されていたプロパティーの値
	 */
	private Object set(String property, Object[] parameters) {
		switch (((null == parameters) ? 0 : parameters.length)) {
		case 1:
			return properties.put(property, parameters[0]);

		case 2:
			return getMap(property).put(parameters[0], parameters[1]);

		default:
			return null;
		}
	}
}
